using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace NDF.XnaLibrary.Models
{
	/// <summary>
	/// Base class for both human playable characters and NPCs.
	/// </summary>
	public class Character : GameModel, IIndependentlyMoving
	{
		#region Fields

		protected Vector3 mPosition;
		protected Quaternion mRotation;
		protected Matrix mWorldMatrix;
		protected CombatStatistics mCombatStatistics;
		protected Vector3 mWalkingDirection;
		protected CastingAction mCastingAction;
		protected CharacterState mState;
		protected Dictionary<AnimationType, Animation> mAnimations;
		protected SkinnedModelInfo mSkinnedModelInfo;
		protected Inventory mInventory = new Inventory();
		protected SpellBook mSpellBook = new SpellBook();
		
		//Use field initializers on the inventory and EquipmentMatrices since otherwise they`ll be null, when the 
		//GameModel constructor is called and will cause NullReferenceException
		protected Dictionary<EquipmentSlot, Matrix> mEquipmentMatrices =
				new Dictionary<EquipmentSlot, Matrix>(EnumEqualityComparers.EquipmentSlotEqualityComparer.Default);

		#endregion

		#region Events

		public delegate void MovingEventHandler(object sender, WalkingEventArgs args);
		public delegate void RotatingEventHandler(object sender, RotatingEventArgs args);
		public delegate void CastingEventHandler(object sender, CastingEventArgs args);

		public event MovingEventHandler Moving;
		public event RotatingEventHandler Rotating;
		public event CastingEventHandler Casting;

		private void OnWalking(WalkingEventArgs args)
		{
			if (Moving != null)
				Moving(this, args);
		}

		private void OnRotating(RotatingEventArgs args)
		{
			if (Rotating != null)
				Rotating(this, args);
		}

		private void OnCasting(CastingEventArgs args)
		{
			if (Casting != null)
				Casting(this, args);
		}

		#endregion

		#region Constructors

		/// <summary>
		/// Initializes a new instance of the Character class with the given parameters. 
		/// </summary>
		/// <param name="content">The content manager to load from.</param>
		/// <param name="assetName">The model to be loaded.</param>
		/// <param name="camera">The game camera.</param>
		public Character(Model model) :
			this(model, null, Vector3.Zero, Quaternion.Identity, Vector3.One, string.Empty)
		{ }

		/// <summary>
		/// Initializes a new instance of the Character class with the given parameters. 
		/// </summary>
		/// <param name="content">The content manager to load from.</param>
		/// <param name="assetName">The model to be loaded.</param>
		/// <param name="camera">The game camera.</param>
		/// <param name="effect">The effect to use.</param>
		public Character(Model model, Effect effect) :
			this(model, effect, Vector3.Zero, Quaternion.Identity, Vector3.One, string.Empty)
		{ }

		/// <summary>
		/// Initializes a new instance of the Character class with the given parameters. 
		/// </summary>
		/// <param name="content">The content manager to load from.</param>
		/// <param name="assetName">The model to be loaded.</param>
		/// <param name="camera">The game camera.</param>
		/// <param name="effect">The effect to use.</param>
		/// <param name="position">The position of the character in world space.</param>
		/// <param name="rotation">The rotation of the character.</param>
		/// <param name="scale">The scale vector of the character.</param>
		/// <param name="name">The name of the character.</param>
		public Character(Model model, Effect effect,
			Vector3 position, Quaternion rotation, Vector3 scale, string name) : 
			base(model, effect, name)
		{
			InitEquipmentMatrices();

			this.mPosition = position;
			this.mRotation = rotation;
			this.mScale = scale;
			this.mWorldMatrix = Matrix.CreateScale(this.mScale) *
				Matrix.CreateFromQuaternion(this.mRotation) *
				Matrix.CreateTranslation(this.mPosition);

			this.mCombatStatistics = new CombatStatistics() { MovementSpeed = 0.1f };

			InitAnimations();
		}

		private void InitAnimations()
		{
			this.mState = CharacterState.Idle;
			this.mCastingAction = new CastingAction();
			this.mAnimations = new Dictionary<AnimationType, Animation>(EnumEqualityComparers.AnimationTypeEqualityComparer.Default);
			this.mAnimations.Add(AnimationType.Idle, Animation.FrozenAnimation);
			this.mAnimations.Add(AnimationType.Moving, Animation.BasicMovingAnimation);
			this.mAnimations.Add(AnimationType.Casting0, Animation.BasicCastingAnimation);
			this.mAnimations.Add(AnimationType.PostCasting0, Animation.BasicCastingAnimation);

			foreach (Animation animation in mAnimations.Values)
			{
				animation.Start(mSkinnedModelInfo);
			}
		}

		private void InitEquipmentMatrices()
		{
			//Set some basic matrices
			mEquipmentMatrices.Add(EquipmentSlot.WeaponSlot1, Matrix.CreateTranslation(1.3f, -2.1f, 0.1f));
		}

		#endregion

		#region Properties

		/// <summary>
		/// Gets or sets the model.
		/// </summary>
		public override Model Model
		{
			get
			{
				return mModel;
			}
			set
			{
				mModel = value;
				mSkinnedModelInfo = mModel.Tag as SkinnedModelInfo;
				mModelBoneTransforms = new Matrix[mSkinnedModelInfo.BindPose.Count];
				mSkinnedModelInfo.BindPose.CopyTo(mModelBoneTransforms);

				mIsLoaded = true;
			}
		}

		/// <summary>
		/// Gets or sets the position of the character.
		/// </summary>
		public Vector3 Position
		{
			get { return mPosition; }
			set 
			{
				mPosition = Utilities.GetPositionInMap(value);
			}
		}

		/// <summary>
		/// Gets or sets the rotation of the character.
		/// </summary>
		public Quaternion Rotation
		{
			get { return mRotation; }
			set
			{
				mRotation = value; 
				OnRotating(new RotatingEventArgs(mRotation));
			}
		}

		/// <summary>
		/// This property is not supported since none should be allowed to change the world matrix. 
		/// <para>This property always throws NotSupportedException.</para>
		/// </summary>
		public Matrix WorldMatrix
		{
			get { throw new NotSupportedException("Reading from the character`s world matrix is not supported"); }
		}

		/// <summary>
		/// Gets the current state of the character.
		/// </summary>
		public CharacterState State
		{
			get { return mState; }
		}

		/// <summary>
		/// Gets the current direction_position at which the character is walking towards.
		/// </summary>
		public Vector3 WalkingDirection
		{
			get { return mWalkingDirection; }
		}

		/// <summary>
		/// Gets the equipped items of the character.
		/// </summary>
		public Inventory Inventory
		{
			get { return mInventory; }
		} 

		/// <summary>
		/// Gets the character`s spell book.
		/// </summary>
		public SpellBook SpellBook
		{
			get { return mSpellBook; }
		}

		/// <summary>
		/// Gets or sets the combat stats of the character.
		/// </summary>
		public CombatStatistics CombatStats
		{
			get { return mCombatStatistics; }
			set { mCombatStatistics = value; }
		}

		#endregion

		#region Methods

		/// <summary>
		/// Saves the changes you`ve made to the character`s effect. 
		/// <para>Call this method each time you modify the character`s effect.</para>
		/// <para>Load the character model prior to using this method.</para>
		/// <exception cref="InvalidOperationException"></exception>
		/// </summary>
		public override void SaveEffectChanges()
		{
			foreach (Equipment item in Inventory.Items)
			{
				item.Effect = Effect;
				item.SaveEffectChanges();
			}
			
			base.SaveEffectChanges(Effect);
		}

		/// <summary>
		/// Commands the character to begin walking in the given direction.
		/// </summary>
		/// <param name="direction">The direction to move to.</param>
		public void StartWalking(Vector3 direction)
		{
			if (mState != CharacterState.Moving && mState != CharacterState.MovingAndPostCasting)
				mAnimations[AnimationType.Moving].Reset();

			if (mState == CharacterState.Casting)
				throw new InvalidOperationException("Cant move while casting");
			else if (mState == CharacterState.PostCasting)
				mState = CharacterState.MovingAndPostCasting;
			else
				mState = CharacterState.Moving;

			direction.Normalize();
			mWalkingDirection = direction;
			OnWalking(new WalkingEventArgs(direction));			
		}

		/// <summary>
		/// Commands the character to stop walking.
		/// </summary>
		public void StopWalking()
		{
			if (mState == CharacterState.MovingAndPostCasting)
				mState = CharacterState.PostCasting;
			else if (mState == CharacterState.Moving)
			{
				mState = CharacterState.Idle;
				mAnimations[AnimationType.Idle].Reset();
			}
			else
				throw new InvalidOperationException("Cannot stop walking, when it hasnt even begun");

			OnWalking(new WalkingEventArgs(mPosition));
		}

		/// <summary>
		/// Commands the character to cast the given skill and run the appropriate animation.
		/// </summary>
		/// <param name="skillID">The id of the skill to be cast.</param>
		/// <exception cref="InvalidOperationException"/>
		public void Cast(int skillID)
		{
			//Check whether the character has learned the given spell.
			if (!mSpellBook.HasLearnedSkill(skillID))
			{
				throw new InvalidOperationException("Cannot cast a skill that the character hasn`t learned");
			}
			//Get the corresponding skill
			Skill skill = mSpellBook.GetSkill(skillID);
			//Check whether you can actually cast (currently moving, another cast in progress or facing the wrong direction_position)
			if (mState == CharacterState.Casting)
			{
				throw new InvalidOperationException("Cannot cast two skills simultaneously");
			}
			else if ((mState == CharacterState.Moving || mState == CharacterState.MovingAndPostCasting) && !skill.IsInstantCast)
			{
				throw new InvalidOperationException("Cannot cast a non-instant cast spell while moving");
			}

			//Face the target
			Vector3 targetPosition = Vector3.Zero;
			if (skill.TargetingType == SkillTargetingType.Character)
			{
				targetPosition = ((Character)skill.Target).Position;
			}
			else if (skill.TargetingType == SkillTargetingType.SkillShot)
			{
				targetPosition = (Vector3)skill.Target;
			}

			//Set the appropriate action
			mCastingAction.Skill = skill;
			mCastingAction.TimePassed = TimeSpan.Zero;
			mAnimations[skill.Animation].Reset();
			mAnimations[skill.PostCastingAnimation].Reset();

			//Set the state to postcasting if the skill is instant or to casting if not
			if (skill.IsInstantCast)
			{
				if (mState == CharacterState.Moving)
				{
					mState = CharacterState.MovingAndPostCasting;
				}
				else
				{
					mState = CharacterState.PostCasting;
				}
			}
			else
			{
				mState = CharacterState.Casting;
			}

			OnCasting(new CastingEventArgs(skill));
		}

		/// <summary>
		/// Rotates the character so that it faces the given target.
		/// This method only works on points lying in the XZ plane (ground).
		/// </summary>
		/// <param name="target">The point in space the character should face.</param>
		/// <param name="movementDirection">The direct</param>
		private void FaceTarget(Vector3 target)
		{
			Vector3 movementDirection = target - mPosition;
			movementDirection.Normalize();
			Vector3 facingDirection = Vector3.Transform(Vector3.Forward, mRotation);
			if (!Utilities.Vector3AlmostEqual(facingDirection, movementDirection, 0.001f))
			{
				float angleCosine = Vector3.Dot(facingDirection, movementDirection);
				float angleInRads = (float)Math.Acos(angleCosine);
				mRotation = Quaternion.CreateFromAxisAngle(Vector3.Up, angleInRads);
			}
		}

		/// <summary>
		/// Updates the model. Without this method, no animation will occur.
		/// </summary>
		/// <param name="gameTime">The current game time.</param>
		public void Update(GameTime gameTime)
		{
			mWorldMatrix = Matrix.CreateScale(this.Scale) *
				Matrix.CreateFromQuaternion(this.Rotation) *
				Matrix.CreateTranslation(this.Position);

			UpdateAnimations(gameTime);
		}

		#region UpdateAnimations

		private void UpdateAnimations(GameTime gameTime)
		{
			switch (mState)
			{
				case CharacterState.Moving:
					{
						UpdateMovingAnimation(gameTime);
						break;
					}
				case CharacterState.Casting:
					{
						UpdateCastingAnimation(gameTime);
						break;
					}
				case CharacterState.PostCasting:
					{
						UpdatePostCastingAnimation(gameTime);
						break;
					}
				case CharacterState.MovingAndPostCasting:
					{
						UpdateMovingAndPostCastingAnimation(gameTime);
						break;
					}
				default:
					{
						mAnimations[AnimationType.Idle].Update(gameTime.ElapsedGameTime);
						mModelBoneTransforms = mAnimations[AnimationType.Idle].FinalBoneTransformations;
						break;
					}
			}
		}

		private void UpdateMovingAnimation(GameTime gameTime)
		{
			mAnimations[AnimationType.Moving].Update(gameTime.ElapsedGameTime);
			mModelBoneTransforms = mAnimations[AnimationType.Moving].FinalBoneTransformations;
			Position += mWalkingDirection * 
				(float)gameTime.ElapsedGameTime.TotalSeconds * 
				mCombatStatistics.MovementSpeed;
		}

		private void UpdateCastingAnimation(GameTime gameTime)
		{
			mAnimations[mCastingAction.Skill.Animation].Update(gameTime.ElapsedGameTime);
			mModelBoneTransforms = mAnimations[mCastingAction.Skill.Animation].FinalBoneTransformations;
			mCastingAction.TimePassed += gameTime.ElapsedGameTime;

			//If the cast is complete, start the PostCasting animation
			if (mCastingAction.TimePassed >= mCastingAction.Skill.CastingTime)
			{
				mState = CharacterState.PostCasting;
				mAnimations[mCastingAction.Skill.PostCastingAnimation].Reset();
			}
		}

		private void UpdatePostCastingAnimation(GameTime gameTime)
		{
			mAnimations[mCastingAction.Skill.PostCastingAnimation].Update(gameTime.ElapsedGameTime);
			mModelBoneTransforms = mAnimations[mCastingAction.Skill.PostCastingAnimation].FinalBoneTransformations;
			mCastingAction.TimePassed += gameTime.ElapsedGameTime;

			if (mCastingAction.TimePassed >= mAnimations[mCastingAction.Skill.PostCastingAnimation].Duration)
			{
				mState = CharacterState.Idle;
				mAnimations[AnimationType.Idle].Reset();
			}
		}

		private void UpdateMovingAndPostCastingAnimation(GameTime gameTime)
		{
			mAnimations[AnimationType.Moving].Update(gameTime.ElapsedGameTime);
			mAnimations[mCastingAction.Skill.PostCastingAnimation].Update(gameTime.ElapsedGameTime);
			mModelBoneTransforms = mAnimations[AnimationType.Moving].FinalBoneTransformations; 
			for (int i = 0; i < mModelBoneTransforms.Length; i++)
			{
				mModelBoneTransforms[i] *= mAnimations[mCastingAction.Skill.PostCastingAnimation].FinalBoneTransformations[i];
			}

			mPosition += mWalkingDirection *
				(float)gameTime.ElapsedGameTime.TotalSeconds *
				mCombatStatistics.MovementSpeed; 
			mCastingAction.TimePassed += gameTime.ElapsedGameTime;

			//If the animation has reached its end, end it
			if (mCastingAction.TimePassed >= mAnimations[mCastingAction.Skill.PostCastingAnimation].Duration)
			{
				//If the state is PostCasting, then the movement has stopped as well
				if (mState == CharacterState.PostCasting)
				{
					mState = CharacterState.Idle;
					mAnimations[AnimationType.Idle].Reset();
				}
				else 
				{
					mState = CharacterState.Moving;
				}
			}
		}
		#endregion

		/// <summary>
		/// Draws the character.
		/// <exception cref="InvalidOperationException"></exception>
		/// </summary>
		public void Draw()
		{
			if (!mIsLoaded)
			{
				throw new InvalidOperationException("Cant draw the character prior to loading it. Load the model first.");
			}

			foreach (ModelMesh mesh in this.mModel.Meshes)
			{
				foreach (Effect effect in mesh.Effects)
				{
					effect.CurrentTechnique = effect.Techniques["Skinned"];
					effect.Parameters["BoneTransforms"].SetValue(mModelBoneTransforms);
					effect.Parameters["World"].SetValue(mWorldMatrix);
					effect.Parameters["View"].SetValue(Camera.Active.ViewMatrix);
					effect.Parameters["Projection"].SetValue(Camera.Active.ProjectionMatrix);
				}
				mesh.Draw();
			}

			DrawInventory(false);
		}

		public void DrawShadowMap()
		{
			if (!mIsLoaded)
			{
				throw new InvalidOperationException("Cant draw the character prior to loading it. Load the model first.");
			}

			foreach (ModelMesh mesh in this.mModel.Meshes)
			{
				foreach (Effect effect in mesh.Effects)
				{
					effect.CurrentTechnique = effect.Techniques["SkinnedShadowMap"];
					effect.Parameters["BoneTransforms"].SetValue(mModelBoneTransforms);
					effect.Parameters["World"].SetValue(mWorldMatrix);
				}
				mesh.Draw();
			}

			DrawInventory(true);			
		}

		private void DrawInventory(bool useShadowMap)
		{
			foreach (Equipment item in mInventory.Items)
			{
				ModelBoneIndex index = SkeletonToEquipmentBinder.EquipmentToBoneIndices[item.Slot];
				int itemParentBoneIndex = mSkinnedModelInfo.SkeletonIndices[index];

				//Find the matrix representing the world transform of the item`s parent bone
				Matrix itemParentBoneTransform;
				switch (mState)
				{
					case CharacterState.Casting:
						itemParentBoneTransform = mAnimations[mCastingAction.Skill.Animation].AbsoluteBoneTransformations[itemParentBoneIndex];
						break;
					case CharacterState.PostCasting:
						itemParentBoneTransform = mAnimations[mCastingAction.Skill.PostCastingAnimation].AbsoluteBoneTransformations[itemParentBoneIndex];
						break;
					case CharacterState.Moving:
						itemParentBoneTransform = mAnimations[AnimationType.Moving].AbsoluteBoneTransformations[itemParentBoneIndex];
						break;
					case CharacterState.MovingAndPostCasting:
						itemParentBoneTransform = mAnimations[AnimationType.Moving].AbsoluteBoneTransformations[itemParentBoneIndex] *
							mAnimations[mCastingAction.Skill.PostCastingAnimation].AbsoluteBoneTransformations[itemParentBoneIndex];
						break;
					default:
						itemParentBoneTransform = mAnimations[AnimationType.Idle].AbsoluteBoneTransformations[itemParentBoneIndex];
						break;
				}
				//The item`s world matrix is the result of its scale, its parent bone world transform, its equipment matrix and the 
				//character`s world matrix
				Matrix itemWorldMatrix =
					 Matrix.CreateScale(item.Scale) *
					 itemParentBoneTransform *
					 mEquipmentMatrices[item.Slot] *
					 mWorldMatrix;

				item.Draw(itemWorldMatrix, useShadowMap);
			}
		}

		#endregion
	}

	public enum CharacterState
	{
		Idle, Moving, Jumping, Casting, PostCasting, MovingAndPostCasting
	}	  

	#region CharacterEventArgs

	//Provides data for NDF.XnaLibrary.Models.Character`s Moving event
	public class WalkingEventArgs : EventArgs
	{
		
		/// <summary>
		/// The direction to start walking toward or the final position of the current movement.
		/// </summary>
		public readonly Vector3 Direction_Position;

		/// <summary>
		/// Initializes a new WalkingEventArgs with the given data.
		/// </summary>
		/// <param name="direction_position">The direction to start walking towards or the final position of the current movement.</param>
		public WalkingEventArgs(Vector3 direction_position)
		{
			Direction_Position = direction_position;
		}
	}

	//Provides data for NDF.XnaLibrary.Models.Character`s Rotating event
	public class RotatingEventArgs : EventArgs
	{
		/// <summary>
		/// The new rotation of the character.
		/// </summary>
		public readonly Quaternion Rotation;

		/// <summary>
		/// Initializes a new RotatingEventArgs with the given data.
		/// </summary>
		/// <param name="rotation">The new rotation of the character.</param>
		public RotatingEventArgs(Quaternion rotation)
		{
			Rotation = rotation;
		}
	}

	//Provides data for NDF.XnaLibrary.Models.Character`s Casting event
	public class CastingEventArgs : EventArgs
	{
		/// <summary>
		/// The skill which the character started casting.
		/// </summary>
		public readonly Skill Skill;

		/// <summary>
		/// Initializes a new CastingEventArgs with the given data.
		/// </summary>
		/// <param name="skill">The skill which the character started casting.</param>
		public CastingEventArgs(Skill skill)
		{
			Skill = skill;
		}
	}

	#endregion
}